更新时间:2024-07-01 13:35
buffer cache,又称bcache,其中文名称为缓冲器高速缓冲存储器。oracle数据库的本质就是能够用磁盘上的文件来存储数据,并提供了各种各样的手段对这些数据进行管理。作为管理数据的最基本要求就是能够保存和读取磁盘上的文件中的数据。众所周知,读取磁盘的速度相对来说是非常慢的,而内存相对速度则要快的多。因此为了能够加快处理数据的速度,oracle必须将读取过的数据缓存在内存里。而这些缓存在内存里的数据就是数据高速缓存区(db buffer cache),通常就叫做buffer cache。按照oracle官方的说法,buffer cache就是一块含有许多数据块的内存区域,而这些数据块主要都是数据文件里的数据块内容的拷贝。通过初始化参数:buffer_cache_size来指定buffer cache的大小。oracle实例一旦启动,该区域大小就被分配好了。buffer cache的内容对应磁盘上一个块(block),块通常为1K,都是连续的。
1、结构:Buffer Cache是SGA的一部分,Oracle利用Buffer Cache来管理data block,Buffer Cache的最终目的就是尽可能的减少磁盘I/O。Buffer Cache中主要有3大结构用来管理Buffer Cache。
Hash Bucket & Hash Chain List:Hash Bucket与Hash Chain List用来实现data block的快速定位。
LRU List:挂载有指向具体的free buffer, pinned buffer以及还没有被移动到write list的dirty buffer等信息。所谓的free buffer就是指没有包含任何数据的buffer,所谓的pinned buffer,就是指当前正在被访问的buffer。
Write(Dirty)List:挂载有指向具体的dirty block的信息。所谓的dirty block,就是指在buffer cache中被修改过但是还没有被写入到磁盘的block。
2、功能:
1)通过缓存数据块,从而加快对磁盘上数据的访问,减少I/O。
2)通过构造CR块,从而提供读一致性功能。
3)通过提供各种lock、latch机制,从而提供多个进程并发访问同一个数据块的功能。
3、大小:buffer cache的内容对应磁盘上一个块(block),块通常为1K,都是连续的。在linux下,为了更有效的使用物理内存,操作系统自动使用所有空闲内存作为Buffer Cache使用。当程序需要更多内存时,操作系统会自动减小Cache的大小。
在linux下,可通过命令cat /proc/meminfo和free-m查看buffer cache的内存使用情况.
在从外存的一页到内存的一页的映射过程中,page cache与buffer cache、swap cache共同实现了高速缓存功能,以下是其简单映射图:
外存的一页(分解为几块,可能不连续)——物理磁盘的磁盘块——内存的buffer Cache——内存的一页(由一个页框划分的几个连续buffer cache构成)——页高缓系统
在这个过程中,物理文件系统与Buffer Cache交互,负责在外围存储设备和Buffer Cache 之间交换数据。
由于bcache位于物理文件系统和块设备驱动程序之间,因此,当物理文件系统需要从块设备上读取数据时,它首先试图从bcache中去读。如果命中,则内核就不必在去访问慢速的块设备。否则如果命中失败,也即数据不在bcache中,则内核从块设备上读取相应的数据块,并将其在bcache中缓存起来,以备下次访问之用。
类似地,但物理文件系统需要向块设备上写数据时,也是先将数据写到相应的缓冲区中,并将这个缓冲区标记为脏(dirty),然后在将来的某些时候将buffer cache中的数据真正地回写到块设备上,或者将该缓冲区直接丢弃。从而实现减少磁盘写操作的频率。
每一个缓冲区都有一个缓冲区头部来唯一地标识与描述该缓冲区。Linux通过数据结构buffer_head来定义缓冲区头部。如下所示(include/linux/fs.h)
struct buffer_head {
/* First cache line: */
struct buffer_head *b_next; /* Hash queue list */
unsigned long b_blocknr; /* block number */
unsigned short b_size; /* block size */
unsigned short b_list; /* List that this buffer appears */
kdev_t b_dev; /* device (B_FREE = free) */
atomic_t b_count; /* users using this block */
kdev_t b_rdev; /* Real device */
unsigned long b_state; /* buffer state bitmap (see above) */
unsigned long b_flushtime; /* Time when (dirty) buffer should be written */
struct buffer_head *b_next_free;/* lru/free list linkage */
struct buffer_head *b_prev_free;/* doubly linked list of buffers */
struct buffer_head *b_this_page;/* circular list of buffers in one page */
struct buffer_head *b_reqnext; /* request queue */
struct buffer_head **b_pprev; /* doubly linked list of hash-queue */
char * b_data; /* pointer to data block (512 byte) */
struct page *b_page; /* the page this bh is mapped to */
void (*b_end_io)(struct buffer_head *bh, int uptodate); /* I/O completion */
void *b_private; /* reserved for b_end_io */
unsigned long b_rsector; /* Real buffer location on disk */
wait_queue_head_t b_wait;
struct inode * b_inode;
struct list_head b_inode_buffers; /* doubly linked list of inode dirty buffers */
};
各字段的含义如下:
1b_next指针:指向哈希链表中的下一个buffer_head对象。
2.b_blocknr:本缓冲区对应的块号(block number)。
3.b_size:以字节计掉的块长度。合法值为:512、1024、2048、4096、8192、16384和32768。
4.b_list:记录这个缓冲区应该出现在哪个链表上。
5.d_dev:缓冲区对应的块所在的块设备标识符(对于位于free_list链表中的缓冲区,b_dev=B_FREE)。
6.b_count:本缓冲区的引用计数。
7.b_rdev:缓冲区对应的块所在的块设备的「真实」标识符。
8.b_state:缓冲区的状态,共有6种:
/* bh state bits */
#define BH_Uptodate 0 /* 1 if the buffer contains valid data */
#define BH_Dirty 1 /* 1 if the buffer is dirty */
#define BH_Lock 2 /* 1 if the buffer is locked */
#define BH_Req 3 /* 0 if the buffer has been invalidated */
#define BH_Mapped 4 /* 1 if the buffer has a disk mapping */
#define BH_New 5 /* 1 if the buffer is new and not yet written out */
#define BH_Protected 6 /* 1 if the buffer is protected */
9.b_flushtime:脏缓冲区必须被回写到磁盘的最后期限值。
10.b_next_free指针:指向lru/free/unused链表中的下一个缓冲区头部对象。
11b_prev_free指针:指向lru/free/unused链表中的前一个缓冲区头部对象。
12b_this_page指针:指向同属一个物理页帧的下一个缓冲区的相应缓冲区头部对象。同属一个物理页帧的所有缓冲区通过这个指针成员链接成一个单向循环链表。
13b_reqnext指针:用于块设备驱动程序的请求链表。
14b_pprev:哈希链表的后向指针。
15b_data指针:指向缓冲区数据块的指针。
16b_page指针:指向缓冲区所在物理页帧的page结构。
17b_rsector:实际设备中原始扇区的个数。
18b_wait:等待这个缓冲区的等待队列。
19b_inode指针:如果缓冲区属于某个索引节点,则这个指针指向所属的inode对象。
20b_inode_buffers指针:如果缓冲区为脏,且又属于某个索引节点,那么就通过这个指针链入inode的i_dirty_buffers链表中。
缓冲区头部对象buffer_head可以被看作是缓冲区的描述符,因此,对bcache中的缓冲区的管理就集中在如何高效地组织处于各种状态下的buffer_head对象上。
缓冲区头部对象buffer_head本身有一个叫做bh__cachep的slab分配器缓存。因此对buffer_head对象的分配与销毁都要通过kmem_cache_alloc()函数和kmem_cache_free()函数来进行。
注意不要把bh_cachep SLAB分配器缓存和缓冲区本身相混淆。前者只是buffer_head对象所使用的内存高速缓存,并不与块设备打交道,而仅仅是一种有效管理buffer_head对象所占用内存的方式。后者则是块设备中的数据块所使用的内存高速缓存。但是这二者又是相互关联的,也即缓冲区缓存的实现是以bh_cachep SLAB分配器缓存为基础的。而我们这里所说的bcache机制包括缓冲区头部和缓冲区本身这两个方面的概念。
bh_cachep定义在fs/dcache.c文件中,并在函数vfs_caches_init()中被初始化,也即通过调用kmem_cache_create()函数来创建bh_cachep这个SLAB分配器缓存。
注:函数vfs_caches_init()的工作就是调用kmem_cache_create()函数来为VFS创建各种SLAB分配器缓存,包括:names_cachep、filp_cachep、dquot_cachep和bh_cachep等四个SLAB分配器缓存。
一个缓冲区头部对象buffer_head总是处于以下四种状态之一:
1未使用(unused)状态:该对象是可用的,但是其b_data指针为NULL,也即这个缓冲区头部没有和一个缓冲区相关联。
2空闲(free)状态:其b_data指针指向一个空闲状态下的缓冲区(也即该缓冲区没有具体对应块设备中哪个数据块);而b_dev域值为B_FREE(值为0xffff)。
3正在使用(inuse)状态:其b_data指针指向一个有效的、正在使用中的缓冲区,而b_dev域则指明了相应的块设备标识符,b_blocknr域则指明了缓冲区所对应的块号。
4异步(async)状态:其b_data域指向一个用来实现page I/O操作的临时缓冲区。4、bcache机制采用了各种链表来组织这些对象 为了有效地管理处于上述这些不同状态下的缓冲区头部对象,bcache机制采用了各种链表来组织这些对象(这一点,bcache机制与VFS的其它cache机制是相同的):
1.哈希链表:所有buffer_head对象都通过其b_next与b_pprev两个指针域链入哈希链表中,从而可以加快对buffer_head对象的查找(lookup)。
2.近期最少使用链表lru_list:每个处在inuse状态下的buffer_head对象都通过b_next_free和b_prev_free这两个指针链入某一个lru_list链表中。
3.空闲链表free_list:每一个处于free状态下的buffer_head对象都根据它所关联的空闲缓冲区的大小链入某个free_list链表中(也是通过b_next_free和b_prev_free这两个指针)。
4.未使用链表unused_list:所有处于unused状态下的buffer_head对象都通过指针域b_next_free和b_prev_free链入unused_list链表中。
5.inode对象的脏缓冲区链表i_dirty_buffers:如果一个脏缓冲区有相关联的inode对象的话,那么他就通过其b_inode_buffers指针域链入其所属的inode对象的i_dirty_buffers链表中。
(更详细的介绍请见参考资料二)
当前台进程发出SELECT或者其他DML语句时,oracle根据SQL语句的执行计划所找到的数据块(根据查询索引或者全表扫描,找到要查询的数据条目所在的块,一般根据ROWID等信息,举个例子:一个简单的select语句,条件是empno=7788,根据索引,oracle会知道7788这个条目的ROWID,然后就能知道数据块的位置,到底是在那个表空间,那个对象,那个数据文件中),会构造一个名为数据块描述(buffer descriptor)的内存结构。该buffer descriptor位于session的PGA中,所包含的内容主要是数据块所在的物理地址(根据ROWID信息的第33-64bit构造出rbda)、数据块的类型、数据块所属对象的object id等信息。
随后,oracle会把对数据块请求的锁定模式以及所构造出来的buffer descriptor传入专门搜索数据块的函数中。在该函数中,oracle根据buffer descriptor所记录的信息,应用hash算法以后,得到要找的数据块所处的hash bucket,也就是确定该数据块在哪条hash chain上。然后,oracle进入该hash chain,从上面所挂的第一个buffer header开始搜索,一直搜索到最后一个buffer header。
在hash chain上搜索的逻辑如下:
1) 比较buffer header上所记录的数据块的地址(rdba),如果不符合,则跳过该buffer header。
2) 跳过状态为CR的buffer header。(说明有别的进程正在进行一致性读,所以才构造了这个cr块,如果我也要找这个块的原块,我需要自己再重新构造一个新的cr块,不会使用这个旧的cr块,如果我不是找这个块的原块,那我不需要构造,所以这两种情况下都是跳过cr块)
3) 如果遇到状态为READING(正在从磁盘上读出的数据块)的buffer header,则等待,一直等到该buffer header的状态改变以后再比较所记录的数据块的地址是否符合。
4) 如果发现数据块地址符合的buffer header,则查看该buffer header是否位于正在使用的列表上,如果是位于正在使用的列表上,则判断已存在的锁定模式与当前所要求的锁定模式是否兼容,如果是兼容的,则返回该buffer header所记录的数据块地址。
5) 如果发现锁定模式不兼容,则根据找到的buffer header所指向的数据块的内容,构建一个新的、内容一样的、数据块状态为XCURRENT(实例以排他方式获取的当前模式数据块)的复制数据块,并且构造一个状态为CR的buffer header,同时该buffer header指向所新建立的复制数据块。然后,返回该复制数据块的地址,并将当前进程号放入该buffer header所处的正在使用的列表上。
6) 如果比较完整个hash chain以后还没发现所要找的buffer header,则从磁盘上读取数据文件。并将读取到的数据块所对应的buffer header挂到hash chain上。
1、 LRU和LRUW链表结构概述
如果在hash chain上没有找到所要的buffer header时,oracle会发出I/O调用,到磁盘上的数据文件中获取数据块,并将该数据块的内容拷贝一份到buffer cache中的内存数据块里。这个时候,如果buffer cache中的内存数据块全都被占用,为了高效的管理buffer cache中的内存数据块,oracle引入了LRU和LRUW等链表等结构。
在前面描述buffer cache结构的图上,也可以看到有两个链表:LRU和LRUW。在介绍LRU和LRUW前,先说明几个概念。
1) 脏数据块(dirty buffer):buffer cache中的内存数据块的内容与数据文件中的数据块的内容不一致。
2) 可用数据块(free buffer):buffer cache中的内存数据块为空或者其内容与数据文件中的一致。注意,可用数据块不一定是空的。
3) 钉住的数据块(ping buffer):当前正在更新的内存数据块。
4) 数据库写进程(DBWR):这是一个很底层的数据库后台进程。既然是后台进程,就表示该进程是不能被用户调用的。由oracle内置的一些事件根据需要启动该进程,该进程用来将脏数据块写入磁盘上的数据文件。
LRU表示Least Recently Used,也就是指最少使用的buffer header链表。LRU链表串连起来的buffer header都指向可用数据块。而LRUW则表示Least Recently Used Write,也叫做dirty list,也就是脏数据块链表,LRUW串起来的都是修改过但是还没有写入数据文件的内存数据块所对应的buffer header。一个buffer header或者挂在LRU上,或者挂在LRUW上,不能同时挂在这两个链表上。
随着硬件技术的发展,电脑的内存越来越大。buffer cache也是越来越大,只用一条LRU和一条LRUW来管理buffer header已经不够用了。同时oracle还引入了多个DBWR后台进程来帮助将buffer cache中的脏数据块写入数据文件,显然,多个DBWR后台进程都去扫描相同的LRUW链表会引起争用。为此oracle引入了working set(工作集)的概念。每个working set都具有它自己的一组LRU和LRUW链表。每个working set都由一个名为“cache buffers lru chain”的latch(也叫做lru latch)来管理,所以从这个意义上说,每一个lru latch就是一个working set。而每个被加载到buffer cache的buffer header都以轮询的方式挂到working set上去。也就是说,当buffer cache加载一个新的数据块时,其对应的buffer header会去找一个可用的lru latch(找这个工作集中的lru列表,将新加载进来的数据块挂到LRU列表上),如果没有找到,则再找下一个lru latch,直到找到为止。如果轮询完所有的lru latch也没能找到可用的lru latch,该进程只有等待latch free等待事件,同时出现在v中,并增加“latch misses”。如果启用了多个DBWR后台进程的话,每个DBWR进程都会对应一个不同的working set,而且每个DBWR只会处理分配给它的working set,不会处理其他的working set。即working set的数量也就是lru latch的数量。而lru latch的数量是由一个隐藏参数:db_block_lru_latches决定的。该参数缺省值为DBWR进程的数量×8。
该参数最小必须为8,如果强行设置比8小的数值,oracle将忽略设置的值,而使用8作为该参数值。
2SQL> startup force
3SQL> show parameter _db_block
4NAME TYPE VALUE
5------------------------------------ ----------- ------------------------------
6_db_block_lru_latches integer 8
2、 深入LRU链表
在8i之前,我们举一个例子。假设buffer cache只能容纳4个数据块,同时只有一个hash chain和一个LRU。当数据库刚刚启动,buffer cache是空的。这时前台进程发出SELECT语句获取数据块时,oracle找一个空的内存数据块,并将其对应的buffer header挂到hash chain上。同时,oracle还会把该buffer header挂到LRU的最尾端。随后前台进程又发出SELECT语句,这时所找到的buffer header在LRU上会挂到前一个buffer header的后面,也就是说第二次SELECT语句所找到的buffer header现在变成了LRU的最尾端了。假设发出4句SELECT以后找到了4个buffer header,从而用完了所有的buffer cache空间。
这个时候,发来第五句SELECT语句。而buffer cache里已经没有空的内存数据块。但是既然需要容纳下第五个数据块,就必然需要找一个可以被替换的内存数据块。这个内存数据块会到LRU上去找。按照oracle设定的近期最少使用的原则,位于LRU最尾端的BH1将成为牺牲者,oracle会把该BH1对应的内存数据块的内容清空,并将当前第五句SQL所获得的数据块的内容拷贝进去。这个时候,BH1就成了LRU的首端,而BH2则成为了LRU的尾端。在这种方式下,经常被访问的数据块可以一直靠近LRU的首端,也就保证了这些数据块可以尽可能的不被替换掉,从而保证了访问的效率。
到了8i以后,oracle引入了一种更加复杂的机制来管理LRU上的数据块。8i以后,LRU和LRUW链表都具有两个子链表,分别叫做辅助链表和主链表。同时还对buffer header增加了一个属性:touch数量,也就是每个buffer header曾经被访问过的次数,来对LRU链表进行管理。oracle每访问一次buffer header,就会将该buffer header上的touch数量增加1,因此,touch数量“近似”的体现了某个内存数据块总共被访问的次数。注意,这只是近似,并不精确。因为touch的增加并没有使用latch来管理并发性。这只是一个大概值,表示趋势的,不用百分百的精确。
读入第一个数据块时,该数据块对应的buffer header会挂到LRU辅助链表(注意,这里是辅助链表,而不是主链表)的最末端,同时touch数量为1。读取第二个不同的数据块时,该数据块对应的buffer header会挂到前一个buffer header的后面,从而位于LRU辅助链表的最末端,同样touch为1。假设4个数据块全都用完以后的LRU链表可以用下图四描述。每个buffer header的touch数量都为1。此时辅助LRU链表都挂满了,而主LRU链表还是空的。前台发出第五句SQL语句,要求返回指定的数据块。这时,oracle发现buffer cache里已经没有空的内存数据块了,于是从辅助LRU链表的尾部开始扫描,也就是从BH1开始扫描,以查找可以被替代的数据块。扫描的过程中按照下面的逻辑来选择被牺牲的(也就是可以被替代的)数据块:
1) 如果被扫描到的buffer header的touch数量小于隐藏参数_db_aging_hot_criteria(该参数缺省为2)的值,则选中该buffer header作为牺牲者,并立即返回该buffer header所含有的数据块的地址。
2) 如果当前buffer header的touch数量大于_db_aging_hot_criteria的值,则不会使用该buffer header。但是如果当前的_db_aging_stay_count的值小于_db_aging_hot_criteri的值,则会将当前该buffer header的touch值赋值给_db_aging_stay_count;否则将当前buffer header的touch数量减掉一半。
按照上述的逻辑,这时将选出BH1作为牺牲者(因为BH1的touch数量为1,小于_db_aging_hot_criteria的值),并将其对应的内存数据块的内容清空,同时将当前第五个数据块的内容拷贝进去。但是这里要注意,这个时候该BH1在LRU链表上的位置并不会发生任何的变化(这里是插入了新的数据块的内容,所以touc的数量没有变化,下面是返回已经有的数据块,所以touch的数量加1了,这样就保证了touch为1的数据块即不常用的数据块一直在辅助链表,而不会跑到主lru链表上)。而不会像8i之前的那样,BH1变成LRU链表的首端。
接下来,前台发来了第六句和第七句SQL,分别要返回与第五句和第四句SQL一样的数据块,也就是要返回当前的BH1和BH4。这个时候,oracle会增加BH1和BH4的touch数量,同时将该BH1和BH4从辅助LRU链表上摘下,转移到主LRU链表的中间位置。
如果发来了第八句SQL,要求返回与第三句SQL相同的数据块,也就是当前的BH3,则这时该BH3会插入主LRU链表上的BH1和BH4中间,注意每次向主LRU列表插入buffer header时都是向中间位置插入。如果发来了第九句SQL要求返回BH2,则我们可以知道,BH2会转移到主LRU链表的中间。这个时候,辅助LRU链表就空了,没有buffer header了。
如果又发来第十句SQL,要求返回一个新的、buffer cache中不存在所需内容的数据块时。oracle会先扫描辅助LRU链表,发现上面没有任何的buffer header时,则必须扫描主LRU链表。从尾部开始扫描,采用前面说到的与扫描辅助LRU链表相同的规则挑选牺牲者。挑出的可以被替代的buffer header将从主LRU链表上摘下,放入辅助LRU链表。
从上面所描述的buffer header在辅助LRU链表和主LRU链表之间交替的过程中,我们可以看出,oracle改进LRU链表的管理方式的目的,就是想千方百计的能够将多次被访问的数据块保留在内存里,同时又要平衡有限的内存资源。这种方式相比较8i之前而言,无疑是进步很多的。在8i之前中,某个数据块可能只会被访问一次,但是就这么一次的访问就将该数据块放到了LRU的首端,从而可能就挤掉了一个LRU上不是那么经常被访问,但是也会多次访问的数据块。而8i以后,将访问一次的数据块和访问一次以上的数据块彻底分开,而且查找可用数据块时,始终都是从辅助LRU链表开始扫描。实际上也就使得越倾向于只访问一次的数据块越快的从内存中清理出去。
3、LRUW链表管理
LRUW表示脏数据块链表,该链表上的buffer header指向的都是已经从LRU链表上摘下来、其对应的内存数据块里的内容已经被修改、但是还没有被写入数据文件的内存数据块。在这些脏数据块在能够被重用之前,它们必须要被DBWR写入磁盘。从8i以后,LRUW链表同样包含两个子链表:辅助LRUW链表和主LRUW链表。
假设前台用户发出DML语句,要求修改BH2所指向的内存数据块。这时,按顺序发生下面的动作:
1)oracle会将BH2从辅助LRU链表上摘下,同时插入主LRU链表的中间,也就是插入BH1和BH4中间,同时增加BH2的touch的数量。(与selectBH2的效果一样, 都会使要查找的块从辅助链表上摘下,放入主LRU链表)
2)将该BH2的标记设置为钉住(ping)。
3)更新BH2对应的内存数据块的内容。
4)更新完以后,取消钉住的标记(在主LRU列表上进行第一次更新)。
5)将BH2从主LRU链表转移到主LRUW链表上。
6)如果这个时候又有进程发出更新BH2所对应的内存数据块的内容,则BH2再次被钉住,更新,取消钉住(可以在主LRUW列表上继续更新)。
7)DBWR启动以后,在扫描主LRUW链表时会将BH2转移到辅助LRUW链表上(必须转移到辅助LURW列表才能写入到磁盘)。
8)DBWR将辅助LRUW链表上的BH2对应的数据块写入数据文件。
9)确认成功写入数据文件以后,将BH2从辅助LRUW链表上转移到辅助LRU链表上(返回到辅助LUR列表)。
可以看到,主LRUW链表上包含的buffer header要么是已经更新完了的数据块,要么是被钉住正在更新的数据块。而当DBWR进程启动以后,它会扫描主LRUW链表,并跳过正在被钉住更新的buffer header,而将已经更新完了的buffer header从主LRUW链表上摘除,并转移到辅助LRUW链表上去。扫描完主LRUW链表,或扫描的buffer header的个数达到一定限度时,DBWR会转到辅助LRUW上,将辅助LRUW上面的buffer header所对应的数据块写入数据文件。所以说,对于辅助链表上的buffer header来说,要么是正在等待被写入的;要么就是已经发出写入请求,正在写入而还没写完的。这里要注意的是,buffer header进入LRUW链表,是从尾端进入;而DBWR扫描LRUW链表时,则是从首端开始。
这里将主LRUW链表和辅助LRUW链表分开,主要就是为了提高DBWR在主LRUW链表上扫描的效率。如果只有主LRUW链表而没有辅助LRUW链表的话,势必造成三种类型buffer header交织在LRUW链表上:
1)正在被钉住更新的buffer header;
2)已经更新完,而正在等待被写入数据文件的buffer header;
3)已经发出写请求,正在写而尚未写完的buffer header。
在这种情况下,必然造成DBWR为了找到第二种类型的buffer header而需要扫描不该扫描的第三种类型的buffer header。(把第三种已经发出写请求,但是还没有写完的BH放到了辅助LRUW列表里,避免了扫描第二种已经更新完成,等待被写入的的BH)
4、DBWR进程
DBWR进程负责将脏数据块写入磁盘。它是一个非常重要的进程,在后台进程中的sid为2,在PMON进程启动以后随即启动。
SQL> select c.sid,a.name,a.description
2 from v a ,v b , v c
3 where a.paddr=b.addr
4 and b.addr = c.paddr;
SID NAME DESCRIPTION
---------- ----- -------------------------------------------
1 PMON process cleanup
2 DBW0 db writer process 0
3 LGWR Redo etc.
4 CKPT checkpoint
………………………………………………………………………………
随着内存的不断增加,1个DBWR进程可能不够用了。所以从8i起,我们可以为系统配置多个DBWR进程。初始化参数:db_writer_processe决定了启动多少个DBWR进程。每个DBWR进程都会分配一个lru latch,也就是说每个DBWR进程对应一个working set。因此oracle建议配置的DBWR进程的数量应该等于lru latch的数量,同时应该小于CPU的数量。系统启动时,就确定好了working set与DBWR进程的对应关系,每个DBWR进程只会将分配给自己的working set上的脏数据块写入数据文件。DBWR作为一个后台进程,只有在某些条件满足了才会触发。这些条件包括:
1) 当进程在辅助LRU链表和主LRU链表上扫描以查找可以覆盖的buffer header时,如果已经扫描的buffer header的数量到达一定的限度(由隐藏参数:_db_block_max_scan_pct决定)时,触发DBWR进程。_db_block_max_scan_pct表示已经扫描的buffer header的个数占整个LRU链表上buffer header总数的百分比。这时,搜索可用buffer header的进程挂起,在v中表现为等待“free buffer wait”事件,同时增加v中的“dirty buffers inspected”的值。
2) 当DBWR在主LRUW链表上查找已经更新完而正在等待被写入数据文件的buffer header时,如果找到的buffer header的数量超过一定限度(由隐藏参数:_db_writer_scan_depth_pct决定)时,DBWR就不再继续往下扫描了,而转到辅助LRUW链表上将其上的脏数据块写入数据文件。_db_writer_scan_depth_pct表示已经扫描的脏数据块的个数占整个主LRUW链表上buffer header总数的百分比。
3) 如果主LRUW链表和辅助LRUW链表上的脏数据块的总数超过一定限度,也将触发DBWR进程。该限度由隐藏参数:_db_large_dirty_queue决定。
4) 发生增量检查点(incremental checkpoint)或完全检查点(complete checkpoint)时触发DBWR。
5) 每隔三秒钟启动一次DBWR。
6) 将表空间设置为离线(offline)状态时触发DBWR。
7) 发出命令:alter tablespace … begin backup,从而将表空间设置为热备份状态时触发DBWR。
8) 将表空间设置为只读状态时,触发DBWR。
9) 删除对象时(比如删除某个表)会触发DBWR。
当DBWR要写脏数据块时,并不是说立即将所有的脏数据块都同时写入磁盘。为了尽量减少物理的I/O的次数,DBWR会将要写的脏数据块所对应的buffer header拷贝到一个名为批量写(write batch)的结构中。每个working set所对应的DBWR进程都可以向该结构里拷贝buffer header。当write batch的buffer header的个数达到一定限额时,才会发生实际的I/O,从而将脏数据块写入磁盘。这个限额为硬件平台所能支持的同时并发的异步I/O的最大数量。8i之前是可以用隐藏参数(_db_block_write_batch)来控制这个限额的。但是8i以后,取消了该参数,而由oracle自己来计算。
5 DBWR、CKPT、LGWR进程之间的合作
将内存数据块写入数据文件实在是一个相当复杂的过程,在这个过程中,首先要保证安全。所谓安全,就是在写的过程中,一旦发生实例崩溃,要有一套完整的机制能够保证用户已经提交的数据不会丢失;其次,在保证安全的基础上,要尽可能的提高效率。众所周知,I/O操作是最昂贵的操作,所以应该尽可能的将脏数据块收集到一定程度以后,再批量写入磁盘中。
直观上最简单的解决方法就是,每当用户提交的时候就将所改变的内存数据块交给DBWR,由其写入数据文件。这样的话,一定能够保证提交的数据不会丢失。但是这种方式效率最为低下,在高并发环境中,一定会引起I/O方面的争用。oracle当然不会采用这种没有扩展性的方式。oracle引入了CKPT和LGWR这两个后台进程,这两个进程与DBWR进程互相合作,提供了既安全又高效的写脏数据块的解决方法。
用户进程每次修改内存数据块时,都会在日志缓冲区(redo buffer)中构造一个相应的重做条目(redo entry),该重做条目描述了被修改的数据块在修改之前和修改之后的值。而LGWR进程则负责将这些重做条目写入联机日志文件。只要重做条目进入了联机日志文件,那么数据的安全就有保障了,否则这些数据都是有安全隐患的。LGWR 是一个必须和前台用户进程通信的进程。LGWR 承担了维护系统数据完整性的任务,它保证了数据在任何情况下都不会丢失。
LGWR将重做条目写入联机日志文件的情况分两种:后台写(background write)和同步写(sync write)。触发后台写的条件有四个:
1)每隔三秒钟,LGWR启动一次;
2)在DBWR启动时,如果发现脏数据块所对应的重做条目还没有写入联机日志文件,则DBWR触发LGWR进程并等待LRWR写完以后才会继续;
3)重做条目的数量达到整个日志缓冲区的1/3时,触发LGWR;
4)重做条目的数量达到1MB时,触发LGWR。
而触发同步写的条件就一个:当用户提交(commit)时,触发LGWR。
假如DBWR在写脏数据块的过程中,突然发生实例崩溃。我们已经知道,用户提交时,oracle是不一定会把提交的数据块写入数据文件的。那么实例崩溃时,必然会有一些已经提交但是还没有被写入数据文件的内存数据块丢失了。当实例再次启动时,oracle需要利用日志文件中记录的重做条目在buffer cache中重新构造出被丢失的数据块,从而完成前滚和回滚的工作,并将丢失的数据块找回来。于是这里就存在一个问题,就是oracle在日志文件中找重做条目时,到底应该找哪些重做条目?换句话说,应该在日志文件中从哪个起点开始往后应用重做条目?注意,这里所指的日志文件可能不止一个日志文件。
因为oracle需要随时预防可能的实例崩溃现象,所以oracle在数据库的正常运行过程中,会不断的定位这个起点,以便在不可预期的实例崩溃中能够最有效的保护并恢复数据。同时,这个起点的选择非常有讲究。首先,这个起点不能太靠前,太靠前意味着要处理很多的重做条目,这样会导致实例再次启动时所进行的恢复的时间太长;其次,这个起点也不能太靠后,太靠后说明只有很少的脏数据块没有被写入数据文件,也就是说前面已经有很多脏数据块被写入了数据文件,那也就意味着只有在DBWR启动的很频繁的情况下,才能使得buffer cache中所残留的脏数据块的数量很少。但很明显,DBWR启动的越频繁,那么所占用的写数据文件的I/O就越严重,那么留给其他操作(比如读取buffer cache中不存在的数据块等)的I/O资源就越少。这显然也是不合理的。
从这里也可以看出,这个起点实际上说明了,在日志文件中位于这个起点之前的重做条目所对应的在buffer cache中的脏数据块已经被写入了数据文件,从而在实例崩溃以后的恢复中不需要去考虑。而这个起点以后的重做条目所对应的脏数据块实际还没有被写入数据文件,如果在实例崩溃以后的恢复中,需要从这个起点开始往后,依次取出日志文件中的重做条目进行恢复。考虑到的内存容量越来越大,buffer cache也越来越大,buffer cache中包含几百万个内存数据块也是很正常的现象的前提下,为了能够最佳的确定这个起点,oracle引入了名为CKPT的后台进程,通常也叫作检查点进程(checkpoint process)。这个进程与DBWR共同合作,从而确定这个起点。同时,这个起点也有一个专门的名字,叫做检查点位置(checkpoint position)。
oracle为了在检查点的算法上更加的具有可扩展性(也就是为了能够在巨大的buffer cache下依然有效工作),引入了检查点队列(checkpoint queue),该队列上串起来的都是脏数据块所对应的buffer header。
而DBWR每次写脏数据块时,也是从检查点队列上扫描脏数据块,并将这些脏数据块实际写入数据文件的。当写完以后,DBWR会将这些已经写入数据文件的脏数据块从检查点队列上摘下来。这样即便是在巨大的buffer cache下工作,CKPT也能够快速的确定哪些脏数据块已经被写入了数据文件,而哪些还没有写入数据文件,显然,只要在检查点队列上的数据块都是还没有写入数据文件的脏数据块。
而且,为了更加有效的处理单实例和多实例(RAC)环境下的表空间的检查点处理,比如将表空间设置为离线状态或者为热备份状态等,oracle还专门引入了文件队列(file queue)。文件队列的原理与检查点队列是一样的,只不过每个数据文件会有一个文件队列,该数据文件所对应的脏数据块会被串在同一个文件队列上;同时为了能够尽量减少实例崩溃后恢复的时间,oracle还引入了增量检查点(incremental checkpoint),从而增加了检查点启动的次数。如果每次检查点启动的间隔时间过长的话,再加上内存很大,可能会使得恢复的时间过长。因为前一次检查点启动以后,标识出了这个起点。然后在第二次检查点启动的过程中,DBWR可能已经将很多脏数据块已经写入了数据文件,而假如在第二次检查点启动之前发生实例崩溃,导致在日志文件中,所标识的起点仍然是上一次检查点启动时所标识的,导致oracle不知道这个起点以后的很多重做条目所对应的脏数据块实际上已经写入了数据文件,从而使得oracle在实例恢复时再次重复的处理一遍,效率低下,浪费时间。
有些是直接写(write-through):数据将被立刻写入磁盘,当然,数据也被放入缓存中。如果写操作是在以后做的,那么该缓存被称为后台写(write-back)。后台写比直接写更有效,但也容易出错:如果机器崩溃,或者突然掉电,缓冲中改变过的数据就被丢失了。如果仍未被写入的数据含有重要的薄记信息,这甚至可能意味着文件系统(如果有的话)已不完整。
针对以上的原因,出现了很多的日志文件系统,数据在缓冲区修改后,同时会被文件系统记录修改信息,这样即使此时系统掉电,系统重启后会首先从日志记录中恢复数据,保证数据不丢失。当然这些问题不在本文的叙述范围。
由于上述原因,在使用适当的关闭过程之前,绝对不要关掉电源,sync命令可以清空(flushes)缓冲,也即,强迫所有未被写的数据写入磁盘,可用以确定所有的写操作都已完成。在传统的 UNIX系统中,有一个叫做update(kupdate)的程序运行于后台,每隔30秒做一次sync操作,因此通常无需手工使用sync命令了。Linux另外有一个后台程序,bdflush,这个程序执行更频繁的但不是全面的同步操作,以避免有时sync的大量磁盘I/O操作所带来的磁盘的突然冻结。
page不会同时存在于buffer cache和page cache。add_page_to_hash_queue将此思想显露无余。buffer_head 定义在fs.h,和文件系统有着更为紧密的关系。从文件读写角度看buffer cache缓存文件系统的管理信息像root entry, inode等,而page cache缓存文件的内容。
注意函数block_read_full_page,虽然位于buffer.c,但并没有使用buffer cache. 但是确实使用了buffer:只是再指定page上创建buffer提交底层驱动读取文件内容.这个流程有两个值得注意的地方:
一是普通file的read通过page cache进行
二是page cache读取的时候不和buffer cache进行同步
三是page cache的确使用了buffer,不过注意,buffer 不是buffer cache。
2.4的改进:page cache和buffer cache耦合得更好了。在2.2里,磁盘文件的读使用page cache,而写绕过page cache,直接使用buffer cache,因此带来了同步的问题:写完之后必须使用update_vm_cache()更新可能有的page cache。2.4中page cache做了比较大的改进,文件可以通过page cache直接写了,page cache优先使用high memory。而且,2.4引入了新的对象:file address space,它包含用来读写一整页数据的方法。这些方法考虑到了inode的更新、page cache处理和临时buffer的使用。page cache和buffer cache的同步问题就消除了。原来使用inode+offset查找page cache变成通过file address space+offset;原来struct page 中的inode成员被address_space类型的mapping成员取代。这个改进还使得匿名内存的共享成为可能(这个在2.2很难实现,许多讨论过)。